# a particular search of the reports
# searches can be saved and run on a regular basis
class Search < ActiveRecord::Base
  has_many(:search_runs, :include => :timespan, :order => "timespans.after_date")
  has_many(:rules)
  
  # converts the given query to canonical format such that any two queries with the same
  # semantics end up with the same representation
  # TODO: implement this
  def self.canonicalize(query)
    query
  end
  
  # canonicalizes the given query and then find_or_creates it
  def self.find_or_create_canonicalized(query)
    find_or_create_by_query(canonicalize(query))
  end
  
  # runs all saved searches
  def self.run_saved
    hits = 0
    find_all_by_is_saved(true).each{|s| hits += s.run_on_chunks}
    hits
  end
  
  # finds all search_runs for the given timespans and this search id
  # returns a hash of timespan_ids => search_runs
  def runs_for_timespans(timespans)
    runs = {}
    cond = timespans.empty? ? "1" : "timespan_id in (#{timespans.collect{|t| t.id}.join(',')})"
    search_runs.find(:all, :conditions => cond).each{|sr| runs[sr.timespan_id] = sr}
    runs
  end
  
  # returns an sql expression corresponding to this search
  # uses the SearchToken class, where most of the hard work is done
  def conditions
    return "" if query.blank?
    
    # split query up into tokens (quoted strings, or individual words, with or without a qualifier)
    tokenlist = query.scan(/((\w+:)?("([^\s]+?\s?)+?"|[^\s]+))/)
    
    # add booleans and clean up
    s2 = "or"
    finaltokenlist = []
    tokenlist.each do |s1|
      # remove quotes because they are used for tokenizing, not content
      s1 = s1[0].gsub("\"","")
      
      # if the token is now empty,skip it
      if s1 == ""
        next
      end
      
      # if this pair of words already part of a boolean expression, don't insert an "and", otherwise default to "and"
      if %w[and or not].include?(s1) || %w[and or not].include?(s2)
        s2 = s1
        finaltokenlist << s1
      else
        s2 = s1
        finaltokenlist << "and"
        finaltokenlist << s1
      end
    end
    
    # get sql fragment for each term
    finaltokenlist.collect {|s| SearchToken.new(s).sql}.join(" ")
  end
  
  
  def mark_saved
    self.is_saved = true
    save
  end

  def unsave
    self.is_saved = false
    save
  end
  
  # runs the search on the existing timespan chunks within a given range
  def run_on_chunks
    # update timespans
    Timespan.ensure_timespans
    
    # get the date limiting how many chunks we're searching on
    chunk_limit = Time.now.utc - 20.minutes
    chunk_limit_str = chunk_limit.strftime("%Y-%m-%d %T")
    
    # get the conditions
    begin
      cond = self.conditions 
    rescue
      return 0
    end
    
    # build the query
    logger.info(query)
    query = "select timespans.id as timespan_id, count(*) as num from reports join sources join timespans
      where reports.source_id=sources.id
        and timespans.is_search_chunk=1 
        and timespans.after_date >= '#{chunk_limit_str}'
        and reports.authored_at >= timespans.after_date
        and reports.authored_at < timespans.before_date
        and (#{cond})
      group by timespans.id"
    
    # run query
    results = Report.find_by_sql(query)
    matched_chunk_ids = results.collect{|r| r.timespan_id.to_i}

    # update search runs based on results (this might need some optimizing (or deleting!))
    hits = 0
    results.each{|r| hits += r.num.to_i; SearchRun.update_run(id, r.timespan_id, r.num.to_i)}
    
    # for each chunk, make sure we have a search_run, even if it's 0
    chunks = Timespan.find_chunks_between(chunk_limit, Time.now.utc) # utc doesn't really matter here
    chunks.each{|c| SearchRun.update_run(id, c.id, 0) unless matched_chunk_ids.include?(c.id) }
    
    # return number of hits
    hits
  end
end
